//-----------------------------------------------------------------------
// <copyright file="PreSharp.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <author>Andy Reeves</author>
// <email>andyr</email>
// <date>2004-09-28</date>
// <summary>Runs PreSharp.</summary>
//-----------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks.Tools
{
    using System;
    using System.IO;
    using System.Xml;
    using System.Collections;
    using System.Xml.XPath;
    using System.Globalization;
    using Microsoft.Win32;
    using Microsoft.Build.Framework;
    using Microsoft.Build.Tasks;
    using System.Text;
    using Microsoft.Build.Utilities;


    /// <summary>
    /// Runs PreSharp (pshparse.exe) against CS projects from MSBuild task
    /// </summary>
    public sealed class PreSharp : ToolTaskBase
    {
        /// <summary>
        /// XPath query string constants
        /// </summary>
        private const string CSPATH = "VisualStudioProject/CSHARP/Files/Include/File[not(@Link)]/@RelPath";
        private const string CSPATHLINKS = "VisualStudioProject/CSHARP/Files/Include/File/@Link";

        /// <summary>
        /// Member variables
        /// </summary>
        private int totalDefects;
        private int totalViolations;
        private ArrayList arrCSFiles;
        private ArrayList arrGlobalExclusions;
        private ArrayList arrCodeExclusions;
        private string currProjfile;
        private string responseFileName;

        /// <summary>
        /// Constructor to initialize arrCSFiles
        /// </summary>
        public PreSharp()
        {
            this.arrCSFiles = new ArrayList();
        }

        /// <summary>
        /// The project files for Presharp to analyze
        /// </summary>
        /// <value>The cspFile files for Presharp to analyze</value>
        [Required]
        public string[] CSProjFiles
        {
            get { return (string[])Bag["CSProjFiles"]; }
            set { Bag["CSProjFiles"] = value; }
        }

        /// <summary>
        /// External assembly references
        /// </summary>
        /// <value>External assembly references</value>
        [Required]
        public string[] References
        {
            get { return ((string[])Bag["References"]); }
            set { Bag["References"] = value; }
        }

        /// <summary>
        /// The number of violations found. Excluding items in exlusions list.
        /// </summary>
        /// <value>The number of violations found.</value>
        [Output]
        public int TotalViolations
        {
            get { return this.totalViolations; }
        }

        /// <summary>
        /// Underlying number of defects found (including defects excluded by the exclusions list).
        /// </summary>
        /// <value>The number of defects (incl exclusions) found.</value>
        [Output]
        public int TotalDefects
        {
            get { return this.totalDefects; }
        }

        /// <summary>
        /// Use to disable certain warning types (list of IDs)
        /// </summary>
        public string CSCNowarn
        {
            get { return (string)Bag["CSCNowarn"]; }
            set { Bag["CSCNowarn"] = value; }
        }

        /// <summary>
        /// Use to set the level of CSC warnings displayed (0-4)
        /// </summary>
        public string CSCWarn
        {
            get { return (string)Bag["CSCWarn"]; }
            set { Bag["CSCWarn"] = value; }
        }

        /// <summary>
        /// If set prints full paths of all .CS files with defects.
        /// </summary>
        /// <value>Defaults to false.</value>
        public bool ShowFullCSPaths
        {
            get { return this.GetBoolParameterWithDefault("ShowFullCSPaths", false); }
            set { Bag["ShowFullCSPaths"] = value; }
        }

        /// <summary>
        /// If set prints full defects list even at Normal verbosity
        /// </summary>
        /// <value>Defaults to false.</value>
        public bool ShowDefects
        {
            get { return this.GetBoolParameterWithDefault("ShowDefects", false); }
            set { Bag["ShowDefects"] = value; }
        }

        /// <summary>
        /// Specified name of defect exclusion file to use
        /// </summary>
        public string[] ExclusionsFile
        {
            get { return (string[])Bag["ExclusionsFile"]; }
            set { Bag["ExclusionsFile"] = value; }
        }

        /// <summary>
        /// Returns name of the PSharp binary
        /// </summary>
        protected override string ToolName
        {
            get { return "pshparse.exe"; }
        }

        /// <summary>
        /// Returns path to Presharp
        /// </summary>
        /// <returns>returns path to Presharp tool</returns>
        protected override string GenerateFullPathToTool()
        {
            //TODO find PreSharp
            throw new TaskException("PreSharp.WhereAmI");

            //string path = Environment.ExpandEnvironmentVariables(@"%SparkleWcpRoot%\Tools\PreSharp\bin");
            //return Path.Combine(path, this.ToolName);
        }

        /// <summary>
        /// Called by msbuild to run PreSharp. We use it
        /// to iterate through the list of cspFile files passed and extract the CS files
        /// that we need to send to PreSharp
        /// </summary>
        protected override void InternalExecute()
        {
            // read the exclusions file for global exclusions
            this.ReadExclusions();

            // Because we could be called with multiple cspFile files
            // we want to do a run of PreSharp for each project file
            foreach (string cspFile in this.CSProjFiles)
            {
                this.currProjfile = cspFile;
                this.arrCSFiles.Clear();

                // get all of the CS files from the Proj file
                this.arrCSFiles.AddRange(this.ReadCSProjFile(cspFile, CSPATH + " | " + CSPATHLINKS));

                // now do the work (assuming some files have been returned)
                if (this.arrCSFiles != null)
                {
                    // first create a temp filename to use as a response file
                    // (the file is created and written in GenerateCommandLineCommands
                    this.responseFileName = Path.GetTempFileName();
                    
                    try
                    {
                        // now call our internal execute function to do the work
                        this.PSInternalExecute(cspFile);
                    }
                    finally
                    {
                        // if we used a response file, delete it afterwards
                        if (System.IO.File.Exists(this.responseFileName))
                        {
                            System.IO.File.Delete(this.responseFileName);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Builds the commandline for this tool
        /// </summary>
        /// <param name="commandLine">The object to append the args to.</param>
        /// <returns>True if successfully added args.</returns>
        protected override void AddCommandLineCommands(CommandLineBuilderExtension commandLine)
        {
            string tempCommandLine = "";

            // let the base class do its stuff
            //TODO removed the base class call don't think its needed AndyR
            //base.GenerateCommandLineCommands(commandLine);

            // Add our switches & parameters

            // list of warning types to exclude from analysis
            if (!string.IsNullOrEmpty(this.CSCNowarn))
            {
                tempCommandLine += " /nowarn:" + this.CSCNowarn;
            }

            // list of warning types to exclude from analysis
            if (!string.IsNullOrEmpty(this.CSCWarn))
            {
                tempCommandLine += " /warn:" + this.CSCWarn;
            }

            // add the name of the output file to generate
            if (!string.IsNullOrEmpty(this.currProjfile))
            {
                tempCommandLine += " /out:\"" + this.currProjfile + "\"";
            }

            // add referenced assemblies
            if (this.References != null)
            {
                // need to split the references into simple file names + Lib path
                // otherwise the command line gets so long that the task can fail
                ArrayList libPaths = new ArrayList();

                foreach (string item in this.References)
                {
                    // if the path for this ref had not yet been added to the
                    // list of Lib paths, add it.
                    if (!libPaths.Contains(Path.GetDirectoryName(item)))
                    {
                        libPaths.Add(Path.GetDirectoryName(item));
                        tempCommandLine += " /lib:\"" + Path.GetDirectoryName(item) + "\"";
                    }

                    // now add the reference (minus the path)
                    tempCommandLine += " /reference:\"" + Path.GetFileName(item) + "\"";
                }
            }

            // add the CS files to the end of the command line
            // (need to wrap the filenames in quotes)
            foreach (string file in this.arrCSFiles)
            {
                tempCommandLine += " \"" + file + "\"";
            }

            // If the command line is too long (> 32k) write it to a response file
            if (tempCommandLine.Length > 32000)
            {
                string responseFileCmdLine;

                // Support high characters in response files by passing in Encoding.UTF8.
                using (StreamWriter responseFileStream = new StreamWriter(this.responseFileName, false, Encoding.UTF8)) // HIGHCHAR: ToolTask response files are UTF8
                {
                    responseFileStream.Write(tempCommandLine);
                }

                // now set the command line to just use the response file
                responseFileCmdLine = "@\"" + this.responseFileName + "\"";
                commandLine.AppendSwitch(responseFileCmdLine);
            } // end command line too long
            else
            {
                // if command line length is OK just set it 
                // as the actual command line to use
                commandLine.AppendSwitch(tempCommandLine);
            }

        }       

        /// <summary>
        /// This does the work of building the command line and calling PreSharp (in base class InternalExecute)
        /// Then we parse the XML output doc
        /// </summary>
        /// <param name="cspFile">string filename of cspFile file being processed</param>
        /// <returns>True if OK</returns>
        private bool PSInternalExecute(string cspFile)
        {
            // run base class
            base.InternalExecute();

            int numViolations = 0;

            string logFile = cspFile + ".presharp.xml";

            this.Log.LogMessage(MessageImportance.Low, String.Format("CS Project: {0}", Path.GetFileName(cspFile)));
            
            try
            {
                // if no XML file - either no defects or PreSharp failed to run
                if (!System.IO.File.Exists(logFile))
                {
                    if (this.ShowDefects)
                    {
                        this.Log.LogMessage(MessageImportance.Normal, "No defects found in " + Path.GetFileName(cspFile));
                    }
                    else
                    {
                        this.Log.LogMessage(MessageImportance.Low, "No defects found in " + Path.GetFileName(cspFile));
                    }
                }
                else
                {
                    // load the PreSharp defects document
                    XmlDocument document = new XmlDocument();
                    document.Load(logFile);

                    XmlNodeList defectList = document.SelectNodes("DEFECTS/DEFECT");

                    if (defectList.Count != 0)
                    {
                        foreach (XmlElement defect in defectList)
                        {
                            XmlNode filePath = defect.SelectNodes("SFA/FILEPATH/text()")[0];
                            XmlNode fileName = defect.SelectNodes("SFA/FILENAME/text()")[0];
                            XmlNode line = defect.SelectNodes("SFA/LINE/text()")[0];
                            XmlNode column = defect.SelectNodes("SFA/COLUMN/text()")[0];
                            XmlNode defectCode = defect.SelectNodes("DEFECTCODE/text()")[0];
                            XmlNode description = defect.SelectNodes("DESCRIPTION/text()")[0];
                            XmlNode funcName = defect.SelectNodes("FUNCTION/text()")[0];
                            XmlNode nameSpace = defect.SelectNodes("NAMESPACE/text()")[0];

                            // some defects are not associated with function name - so if this is null
                            // we use the namespace+CS file name as the ID to check for exclusions.
                            string codeID;
                            if (funcName != null)
                            {
                                codeID = funcName.Value;
                            }
                            else
                            {
                                codeID = nameSpace.Value + "\\" + fileName.Value;
                            }

                            // If the defect is not in the exclusions file - process it
                            if (!this.IsExcluded(codeID, defectCode.Value))
                            {
                                StringWriter writer = new StringWriter();

                                writer.Write(nameSpace.Value);
                                writer.Write(",");
                                
                                // if "ShowFullCSPaths" = true, preprend full path
                                if (this.ShowFullCSPaths)
                                {
                                    writer.Write(filePath.Value);
                                }

                                writer.Write(fileName.Value);
                                writer.Write(",");
                                if (funcName != null)
                                {
                                    writer.Write(funcName.Value);
                                }
                                
                                writer.Write(",");
                                writer.Write(line.Value);
                                writer.Write(",");
                                writer.Write(column.Value);
                                writer.Write(",");
                                writer.Write(defectCode.Value);
                                writer.Write(",\"");
                                writer.Write(description.Value);
                                writer.Write("\"");

                                if (this.ShowDefects)
                                {
                                    this.Log.LogMessage(MessageImportance.Normal, writer.ToString());
                                }
                                else
                                {
                                    this.Log.LogMessage(MessageImportance.Low, writer.ToString());
                                }

                                // track number of non-exluded defects
                                numViolations += 1;
                            } // end if InExclusionsFile
                        } // end if ForEach

                        // track overall total violations (after exclusions) and overall defects (inc exclusions)
                        this.totalViolations += numViolations;
                        this.totalDefects += defectList.Count;

                        // show defect totals if non-zero
                        if (numViolations > 0)
                        {
                            this.Log.LogMessage(MessageImportance.Normal, numViolations + " PreSharp defect(s) in " + Path.GetFileName(cspFile));
                        }
                    } // end if defectlist.count > 0
                } // end else exists(logFile)
            } // end try
            catch (Exception e)
            {
                this.Log.LogMessage(MessageImportance.High, String.Format("Exception parsing Presharp output file: {0} Exception:{1}", Path.GetFileName(cspFile), e.Message));
                return false;
            }
            return true;
        }

        /// <summary>
        /// Returns true if a node matching defectcode (and funcName/CodeID) are in exclusions XML doc
        /// </summary>
        /// <param name="funcName">Defect location identifier (function name or namespace+file)</param>
        /// <param name="defect">Presharp Defect code</param>
        /// <returns>True if match in xmlExclusions, otherwise false.</returns>
        private bool IsExcluded(string funcName, string defect)
        {
            // if the parameters are null or there are no items in the exclusions file
            // always return false
            if (string.IsNullOrEmpty(funcName) || string.IsNullOrEmpty(defect))
            {
                return false;
            }

            // Check to see if all Defects of this Defectcode are excluded
            if (this.arrGlobalExclusions != null && this.arrGlobalExclusions.Contains(defect))
            {
                return true;
            }

            // Now check for this specific defect instance
            if (this.arrCodeExclusions != null && this.arrCodeExclusions.Contains(defect + funcName))
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Reads defect exclusions from a file into an Arraylist
        /// </summary>
        private void ReadExclusions()
        {
            // make sure the exclusions lists are nulled
            this.arrGlobalExclusions = null;
            this.arrCodeExclusions = null;

            try
            {
                // if array is null or empty bail out here
                if (this.ExclusionsFile == null || this.ExclusionsFile.GetLength(0) == 0)
                {
                    return;
                }

                // create an XML doc to load each file into
                XmlDocument document = new XmlDocument();
                
                // Allocate an arraylist to put the Global exclusions in
                this.arrGlobalExclusions = new ArrayList();
                
                // and another to put the code exclusions in
                this.arrCodeExclusions = new ArrayList();

                foreach (string file in this.ExclusionsFile)
                {
                    // make sure we have a string that is really a file
                    if (!string.IsNullOrEmpty(file) && System.IO.File.Exists(file))
                    {
                        this.Log.LogMessage(MessageImportance.Low, "Reading exclusions file {0}", this.ExclusionsFile);
                        
                        // Load file in XMLdoc
                        document.Load(file);

                        // Read Global exclusions list
                        XmlNodeList defectList = document.SelectNodes("PreSharpExclusions/GlobalExclusions/GlobalExclusion");
                        if (defectList.Count != 0)
                        {
                            // step through the list adding the exclusions to the arraylist
                            foreach (XmlElement defect in defectList)
                            {
                                XmlNode defectCode = defect.SelectNodes("DEFECTCODE/text()")[0];
                                this.arrGlobalExclusions.Add(defectCode.Value);
                            }
                        }

                        // Read Code exclusions
                        defectList = document.SelectNodes("PreSharpExclusions/CodeExclusions/CodeExclusion");
                        if (defectList.Count != 0)
                        {
                            // step through the list adding the exclusions to the arraylist
                            foreach (XmlElement defect in defectList)
                            {
                                XmlNode defectCode = defect.SelectNodes("DEFECTCODE/text()")[0];
                                XmlNode codeID = defect.SelectNodes("CODEID/text()")[0];
                                this.arrCodeExclusions.Add(defectCode.Value + codeID.Value);
                            }
                        }
                    }   // if file name != null and file exists
                }   // foreach file
            }
            catch (XmlException e)
            {
                this.Log.LogMessage(MessageImportance.High, String.Format("XML Exception parsing exclusions list: {0}, Line:{1}, Pos:{2}", e.Message, e.LineNumber, e.LinePosition));
                return;
            }
            catch (XPathException xpe)
            {
                this.Log.LogMessage(MessageImportance.High, String.Format("Exception parsing exclusions list: {0}", xpe.Message));
                return;
            }
        }

        /// <summary>
        /// Reads a cspFile file and returns an array list
        /// of .CS files found in the file
        /// </summary>
        /// <param name="cspDocument">The fully qualified pathname of the cspFile file to read</param>
        /// <param name="xpathQuery">The XPath query to run against the file</param>
        /// <returns>Arraylist of CS file to process</returns>
        private ArrayList ReadCSProjFile(string cspDocument, string xpathQuery)
        {
            try
            {
                // Create the XPath document.
                XPathDocument myxpathDocument = new XPathDocument(cspDocument);

                //Create an XPathNavigator
                XPathNavigator myxpathNavigator = myxpathDocument.CreateNavigator();

                // Ensure we are at the root node
                myxpathNavigator.MoveToRoot();

                XPathExpression myxpathExpr = myxpathNavigator.Compile(xpathQuery);

                // Create an XPathNodeIterator to walk over the selected nodes
                XPathNodeIterator myxpathNodeiterator = myxpathNavigator.Select(myxpathExpr);

                // get an arraylist to put the results in
                ArrayList arrList = new ArrayList();

                // get the directory of the cspFile file so that we can combine this with 
                // the relative paths of the .CS files to produce absolute file names
                string projPath = Path.GetDirectoryName(cspDocument);

                // iterate through the XPath query results
                while (myxpathNodeiterator.MoveNext())
                {
                    // if the item ends with .CS ...
                    if (myxpathNodeiterator.Current.Value.ToLower().Trim().EndsWith(@".cs"))
                    {
                        // ...combine the file with the full path and add it to the arraylist
                        string csfullPath = this.CombinePaths(projPath, myxpathNodeiterator.Current.Value);
                        arrList.Add(csfullPath);
                    }
                }
                return arrList;
            }
            catch (XmlException e)
            {
                this.Log.LogMessage(MessageImportance.High, String.Format("XML Exception parsing CS Project file: {0}, Exception: {1}, Line/pos:{1/2}", Path.GetFileName(cspDocument), e.Message, e.LineNumber, e.LinePosition));
                return null;
            }
            catch (XPathException xpe)
            {
                this.Log.LogMessage(MessageImportance.High, String.Format("Exception parsing CS Project file: {0}, Exception: {1}", Path.GetFileName(cspDocument), xpe.Message));
                return null;
            }
        }

        /// <summary>
        /// Adds 2 paths and canonicalises the result
        /// </summary>
        /// <param name="p1">Path 1 (root part)</param>
        /// <param name="p2">Path 2 (stem part)</param>
        /// <returns>string Resultant path</returns>
        private string CombinePaths(string p1, string p2)
        {
            try
            {
                string fullpath = Path.Combine(p1, p2);
                return Path.GetFullPath(fullpath);
            }
            catch (Exception e)
            {
                this.Log.LogMessage(MessageImportance.High, String.Format("You cannot combine '{0}' and '{1}' because: {2}{3}", p1, p2, Environment.NewLine, e.Message));
                return null;
            }
        }
    }
}
